package org.ms.mtx.core.aspect;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.ms.mtx.core.Constants;
import org.ms.mtx.core.annotation.Compensable;
import org.ms.mtx.core.annotation.Transactional;
import org.ms.mtx.core.configuration.TransactionProperties;
import org.ms.mtx.core.context.MethodInvokeContext;
import org.ms.mtx.core.context.TransactionContextHolder;
import org.ms.mtx.core.exception.TransactionalException;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * The Abstract Aspect
 *
 * @author lry
 */
@Aspect
public abstract class AbstractAspect {

    private ThreadPoolExecutor threadPoolExecutor;

    /**
     * The get transaction properties
     *
     * @return {@link TransactionProperties}
     */
    public TransactionProperties getTransactionProperties() {
        return new TransactionProperties();
    }

    /**
     * The initialize thread pool executor
     */
    @PostConstruct
    public void initialize() {
        TransactionProperties properties = this.getTransactionProperties();
        this.threadPoolExecutor = new ThreadPoolExecutor(
                properties.getCorePoolSize(), properties.getMaxPoolSize(),
                properties.getKeepAliveTime(), TimeUnit.MILLISECONDS, new SynchronousQueue<>(),
                new ThreadFactory() {
                    private int counter = 0;

                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread();
                        t.setName("transaction-cancel-executor-" + (counter++));
                        t.setDaemon(true);
                        return t;
                    }
                });
    }

    /**
     * The destroy thread pool executor
     */
    @PreDestroy
    public void destroy() {
        if (threadPoolExecutor != null) {
            threadPoolExecutor.shutdown();
        }
    }

    /**
     * The execute cancel transaction
     *
     * @param runnable {@link Runnable}
     */
    private void execute(Runnable runnable) {
        if (threadPoolExecutor == null) {
            throw new RuntimeException("The threadPoolExecutor is null");
        }

        threadPoolExecutor.execute(runnable);
    }

    // ================== @Compensable Aspect

    /**
     * The definition transaction pointcut by {@link Compensable}
     */
    @Pointcut("@annotation(org.ms.mtx.core.annotation.Compensable)")
    public void compensablePointcut() {
    }

    /**
     * The definition transaction around
     *
     * @param pjp {@link ProceedingJoinPoint}
     * @return call result object
     * @throws Throwable exception
     */
    @Around("compensablePointcut()")
    public Object compensableAround(ProceedingJoinPoint pjp) throws Throwable {
        return this.executeCompensableAround(pjp);
    }

    // ================== @Transactional Aspect

    /**
     * The definition transaction pointcut by {@link Transactional}
     */
    @Pointcut("@annotation(org.ms.mtx.core.annotation.Transactional)")
    public void transactionalPointcut() {
    }

    /**
     * The definition transaction around
     *
     * @param pjp {@link ProceedingJoinPoint}
     * @return call result object
     * @throws Throwable exception
     */
    @Around("transactionalPointcut()")
    public Object transactionalAround(ProceedingJoinPoint pjp) throws Throwable {
        return this.executeTransactionalAround(pjp);
    }

    // ================== execute @Compensable and @Transactional around

    /**
     * The definition transaction around
     *
     * @param pjp {@link ProceedingJoinPoint}
     * @return call result object
     * @throws Throwable exception
     */
    private Object executeCompensableAround(ProceedingJoinPoint pjp) throws Throwable {
        // the get Method
        Method method = this.getMethod(pjp);
        Compensable compensable = method.getAnnotation(Compensable.class);
        String txId = StringUtils.defaultIfBlank(compensable.value(), this.getMethodId(method));

        try {
            // build context
            TransactionContextHolder.build(txId, compensable);
            // execute proceed
            return pjp.proceed();
        } catch (Throwable t) {
            // execute cancel
            if (!compensable.async()) {
                TransactionContextHolder.cancel();
            } else {
                Map<String, MethodInvokeContext> cancels = TransactionContextHolder.get().copyCancels();
                this.execute(() -> TransactionContextHolder.cancel(cancels));
            }

            // throw exception
            throw t;
        } finally {
            // execute clear context
            TransactionContextHolder.remove();
        }
    }

    /**
     * The definition transaction around
     *
     * @param pjp {@link ProceedingJoinPoint}
     * @return call result object
     * @throws Throwable exception
     */
    private Object executeTransactionalAround(ProceedingJoinPoint pjp) throws Throwable {
        // the get Method
        Method method = this.getMethod(pjp);
        Object[] parameters = pjp.getArgs();
        Class<?>[] parameterTypes = method.getParameterTypes();
        String methodName = method.getName();

        // the get annotation @Transactional
        Transactional transactional = method.getAnnotation(Transactional.class);
        String confirmMethodName = StringUtils.defaultIfBlank(transactional.confirm(), methodName + Constants.CONFIRM_SUFFIX);
        String cancelMethodName = StringUtils.defaultIfBlank(transactional.cancel(), methodName + Constants.CANCEL_SUFFIX);

        // the build intermediate data
        Object confirmObject = this.getConfirmObject(transactional, pjp);
        Object cancelObject = this.getCancelObject(transactional, pjp);
        MethodInvokeContext methodInvokeContext = new MethodInvokeContext(cancelObject, cancelMethodName, parameterTypes, parameters);

        // the invoke try
        pjp.proceed();

        try {
            // the invoke confirm
            Object result = this.doMethodInvoke(confirmObject, confirmMethodName, parameterTypes, parameters);
            // add cancel event
            this.addCancelEvent(transactional, methodInvokeContext, method);
            return result;
        } catch (TransactionalException e) {
            // add cancel event
            this.addCancelEvent(transactional, methodInvokeContext, method);
            throw e;
        } catch (Throwable t) {
            if (t.getCause() instanceof TransactionalException) {
                // add cancel event
                this.addCancelEvent(transactional, methodInvokeContext, method);
            }

            throw t;
        }
    }

    // ================== get public or confirm/cancel object class

    protected Object getObjectByClass(Class<?> clazz) {
        throw new IllegalStateException("You must be override method of getObjectByClass()");
    }

    /**
     * The get default now Object
     *
     * @param transactional {@link Transactional}
     * @param pjp           {@link ProceedingJoinPoint}
     * @return bean
     */
    private Object getObject(Transactional transactional, ProceedingJoinPoint pjp) {
        if (transactional.clazz() == Void.class) {
            return pjp.getThis();
        }

        return this.getObjectByClass(transactional.clazz());
    }

    /**
     * The get confirm now Object
     *
     * @param transactional {@link Transactional}
     * @param pjp           {@link ProceedingJoinPoint}
     * @return bean
     */
    private Object getConfirmObject(Transactional transactional, ProceedingJoinPoint pjp) {
        if (transactional.confirmClass() == Void.class) {
            return this.getObject(transactional, pjp);
        }

        return this.getObjectByClass(transactional.confirmClass());
    }

    /**
     * The get cancel now Object
     *
     * @param transactional {@link Transactional}
     * @param pjp           {@link ProceedingJoinPoint}
     * @return bean
     */
    private Object getCancelObject(Transactional transactional, ProceedingJoinPoint pjp) {
        if (transactional.cancelClass() == Void.class) {
            return this.getObject(transactional, pjp);
        }

        return this.getObjectByClass(transactional.cancelClass());
    }

    // ================== other

    /**
     * The execute invoke
     *
     * @param object     bean
     * @param methodName method name
     * @param types      parameter types
     * @param parameters parameters
     * @return call result object
     * @throws Throwable exception
     */
    private Object doMethodInvoke(Object object, String methodName, Class<?>[] types, Object[] parameters) throws Throwable {
        Method method = object.getClass().getDeclaredMethod(methodName, types);
        method.setAccessible(true);
        return method.invoke(object, parameters);
    }

    /**
     * The add cancel event
     *
     * @param transactional       {@link Transactional}
     * @param methodInvokeContext {@link MethodInvokeContext}
     * @param method              this method
     */
    private void addCancelEvent(Transactional transactional, MethodInvokeContext methodInvokeContext, Method method) {
        String cancelKey = StringUtils.defaultIfBlank(transactional.value(), this.getMethodId(method));
        TransactionContextHolder.addCancel(cancelKey, methodInvokeContext);
    }

    /**
     * The get Method by ProceedingJoinPoint
     *
     * @param pjp The {@link ProceedingJoinPoint}
     * @return The {@link Method}
     * @throws NoSuchMethodException The {@link NoSuchMethodException}
     */
    private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        return pjp.getTarget().getClass().getMethod(signature.getName(), signature.getParameterTypes());
    }

    /**
     * The get method id
     *
     * @param method {@link Method}
     * @return method id
     */
    private String getMethodId(Method method) {
        return method.toString().replace(" ", "@");
    }

}
